package com.morpheus.redis.lettuce.config;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.morpheus.redis.lettuce.annotation.ConditionalOnLettuceClusterConfig;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger;

@Configuration
@EnableConfigurationProperties({ RedisProperties.class, LettuceProperties.class })
@ConditionalOnClass({ RedisOperations.class, LettuceConnectionFactory.class, ClusterClientOptions.class })
@ConditionalOnLettuceClusterConfig
@Order(1)
public class LettuceClusterAutoConfiguration {
	private static final Logger LOGGER = LoggerFactory.getLogger(LettuceClusterAutoConfiguration.class);
	private static final String DEFAULT_JMX_NAME_BASE = "lettuce";
	private static final String DEFAULT_JMX_NAME_PREFIX = "jmx";
	public static final String KEY_REDIS_CLUSTER_NODES = "spring.redis.cluster.nodes";
	public static final String KEY_REDIS_CLUSTER_TIMEOUT = "spring.redis.cluster.timeout";
	public static final String KEY_REDIS_CLUSTER_MAXREDIRECTS = "spring.redis.cluster.max-redirects";

	@Value("${spring.application.name}")
	private String appName = null;

	@SuppressWarnings("all")
	@Bean
	public GenericObjectPoolConfig<?> poolConfig(RedisProperties redisProperties, LettuceProperties lettuceProperties) {
		LOGGER.debug("redisProperties={}, lettuceProperties={}", redisProperties, lettuceProperties);
		GenericObjectPoolConfig<?> poolConfig = new GenericObjectPoolConfig();
		poolConfig.setJmxEnabled(lettuceProperties.getJmx().isEnabled());
		poolConfig.setJmxNameBase(
				Optional.ofNullable(lettuceProperties.getJmx().getName().getBase()).orElse(DEFAULT_JMX_NAME_BASE));
		poolConfig.setJmxNamePrefix(
				Optional.ofNullable(lettuceProperties.getJmx().getName().getPrefix()).orElse(DEFAULT_JMX_NAME_PREFIX));
		poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
		poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
		poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
		poolConfig.setMaxWaitMillis(redisProperties.getLettuce().getPool().getMaxWait().toMillis());
		poolConfig.setTimeBetweenEvictionRunsMillis(
				redisProperties.getLettuce().getPool().getTimeBetweenEvictionRuns().toMillis());
		poolConfig.setTestOnBorrow(lettuceProperties.getTest().isOnBorrow());
		poolConfig.setTestOnCreate(lettuceProperties.getTest().isOnCreate());
		poolConfig.setTestOnReturn(lettuceProperties.getTest().isOnReturn());
		poolConfig.setTestWhileIdle(lettuceProperties.getTest().isWhileIdle());
		return poolConfig;
	}

	private LettuceClientConfiguration getLettuceClientConfiguration(GenericObjectPoolConfig<?> poolConfig,
			LettuceProperties lettuceProperties) {
		ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
				.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(lettuceProperties.getAdaptiveRefreshTimeout()))
				.enableAdaptiveRefreshTrigger(RefreshTrigger.ASK_REDIRECT, RefreshTrigger.UNKNOWN_NODE)
				.enablePeriodicRefresh(Duration
						.ofSeconds(Optional.ofNullable(lettuceProperties.getPeriodicRefreshTimeout()).orElse(30)))
				.build();
		ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
				.topologyRefreshOptions(topologyRefreshOptions)
				.disconnectedBehavior(ClientOptions.DisconnectedBehavior.ACCEPT_COMMANDS).autoReconnect(true)
				.socketOptions(SocketOptions.builder().keepAlive(true).build()).validateClusterNodeMembership(false)
				.build();
		return LettucePoolingClientConfiguration.builder().poolConfig(poolConfig)
				.clientOptions(clusterClientOptions)
				.readFrom(ReadFrom.SLAVE_PREFERRED)
				.commandTimeout(Duration.ofSeconds(30))
				.shutdownTimeout(Duration.ofSeconds(60))
				.clientName(this.appName).build();
	}

	@Bean
	public RedisClusterConfiguration redisClusterConfiguration(RedisProperties redisProperties) {
		List<String> clusterNodeList = redisProperties.getCluster().getNodes();
		Set<RedisNode> redisNodes = this.getRedisNodes(clusterNodeList);
		RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
				redisProperties.getCluster().getNodes());
		redisClusterConfiguration.setClusterNodes(redisNodes);
		if (redisProperties.getCluster().getMaxRedirects() != null) {
			redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
		}
		if (redisProperties.getPassword() != null) {
			redisClusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
		}
		LOGGER.debug("LettuceClusterAutoConfiguration.redisClusterConfiguration() redisClusterConfiguration={}",
				redisClusterConfiguration);
		return redisClusterConfiguration;
	}

	@SuppressWarnings("all")
	@Bean(destroyMethod = "destroy")
	public RedisConnectionFactory connectionFactory(RedisClusterConfiguration redisClusterConfiguration,
			GenericObjectPoolConfig poolConfig, LettuceProperties lettuceProperties) {
		LettuceClientConfiguration lettuceClientConfiguration = this.getLettuceClientConfiguration(poolConfig,
				lettuceProperties);
		LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisClusterConfiguration,
				lettuceClientConfiguration);
		return connectionFactory;
	}

	@Bean
	@ConditionalOnBean({ LettuceClusterAutoConfiguration.class })
	public StringRedisTemplate stringRedisTemplate(
			@Qualifier("connectionFactory") RedisConnectionFactory connectionFactory) {
		StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(connectionFactory);
		return stringRedisTemplate;
	}

	@Bean(name = "redisTemplate")
	@ConditionalOnBean({ LettuceClusterAutoConfiguration.class })
	// @ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<String, Object> redisTemplate(
			@Qualifier("connectionFactory") RedisConnectionFactory connectionFactory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setConnectionFactory(connectionFactory);
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		return redisTemplate;
	}

	@SuppressWarnings("all")
	public Set<RedisNode> getRedisNodes(List<String> nodeList) {
		nodeList = Optional.ofNullable(nodeList).orElse(new ArrayList<>());
		Set<RedisNode> redisNodes = new HashSet<RedisNode>();
		for (String node : nodeList) {
			String host = StringUtils.trimToEmpty(StringUtils.splitByWholeSeparator(node, ":")[0]);
			String strPort = StringUtils.trimToEmpty(StringUtils.splitByWholeSeparator(node, ":")[1]);
			if (StringUtils.isNotBlank(host) && NumberUtils.isCreatable(strPort)) {
				int port = Integer.valueOf(strPort);
				RedisNode redisNode = new RedisNode(host, port);
				redisNodes.add(redisNode);
			}
		}
		return redisNodes;
	}
}
